package com.neuronbit.businessflow.flow.project;

import com.neuronbit.businessflow.flow.BpmFile;
import com.neuronbit.businessflow.flow.clazz.ClassInfo;
import com.neuronbit.businessflow.flow.clazz.SourceClassInfo2;
import com.neuronbit.javaparser.JavaParser;
import com.neuronbit.javaparser.ParseResult;
import com.neuronbit.javaparser.ast.CompilationUnit;
import com.neuronbit.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration;
import com.neuronbit.javaparser.resolution.model.SymbolReference;
import com.neuronbit.javaparser.symbolsolver.JavaSymbolSolver;
import com.neuronbit.javaparser.symbolsolver.resolution.typesolvers.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

import javax.xml.stream.XMLStreamException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;

import static java.lang.String.format;

/**
 * The type Gradle project.
 */
@Slf4j
public abstract class AbstractProject implements Project {
    /**
     * The Path.
     */
    protected Path path;
    private Set<File> srcDirs;
    @Getter
    private Set<File> includeDirs;
    /**
     * The Name.
     */
    @Getter
    protected String name;
    private final String packages;
    @Getter
    private final String[] include;
    /**
     * The Sub system id.
     */
    @Getter
    protected String subSystemId;
    /**
     * The Api definition paths.
     */
    @Getter
    protected List<File> apiDefinitionPaths;

    private volatile JavaParser javaParser;
    private CombinedTypeSolver combinedTypeSolver;

    /**
     * Instantiates a new Gradle project.
     *
     * @param packages the packages
     * @throws IOException any io related exception
     */
    protected AbstractProject(String packages) throws IOException {
        this(packages, "");
    }


    /**
     * Instantiates a new Gradle project.
     *
     * @param packages packages which contains API definitions
     * @param include  scan the packages in the following artifacts, separate multiple artifact name by comma.
     * @throws IOException any io related exception
     */
    protected AbstractProject(String packages, String include) throws IOException {
        this.packages = packages;
        this.include = StringUtils.isBlank(include) ? new String[0] : include.replaceAll("\\s", "").split(",");
    }

    /**
     * Build project.
     *
     * @return the project
     * @throws IOException the io exception
     */
    public Project build() throws IOException {
        this.srcDirs = resolveSrcDirs();

        this.includeDirs = resolveIncludeDirs();

        this.path = getProjectPath();
        this.name = getProjectName();
        this.apiDefinitionPaths = initPackages(packages, srcDirs, includeDirs);

        getJavaParser();
        return this;
    }

    /**
     * Gets project name.
     *
     * @return the project name
     */
    protected abstract String getProjectName();

    /**
     * Gets project path.
     *
     * @return the project path
     */
    protected abstract Path getProjectPath();

    /**
     * Resolve src dirs set.
     *
     * @return the set
     */
    protected abstract Set<File> resolveSrcDirs();

    /**
     * Resolve additional api dirs set.
     *
     * @return the set
     * @throws IOException the io exception
     */
    protected abstract Set<File> resolveIncludeDirs() throws IOException;

    /**
     * Init packages list.
     *
     * @param packages     the packages
     * @param srcDirsArray the src dirs array
     * @return the list
     */
    protected List<File> initPackages(String packages, Set<File>... srcDirsArray) {
        if (StringUtils.isBlank(packages)) {
            if (null != srcDirsArray) {
                List<File> result = new ArrayList<>();
                for (Set<File> files : srcDirsArray) {
                    for (File file : files) {
                        if (null != file) {
                            result.add(file);
                        }
                    }
                }
                return result;
            } else {
                return Collections.emptyList();
            }
        }

        List<File> packageFiles = new ArrayList<>();
        for (String packageName : packages.split(",")) {
            final String[] packageSeg = packageName.split("\\.");
            boolean exits = false;
            for (Set<File> dirs : srcDirsArray) {
                for (File srcDir : dirs) {
                    final File packageFile = srcDir.toPath().resolve(String.join(File.separator, packageSeg)).toFile();
                    if (packageFile.exists()) {
                        exits = true;
                        packageFiles.add(packageFile);
                    }
                }

                if (!exits) {
                    throw new IllegalArgumentException(format("can not find file from %s which mapped from configured package %s",
                            dirs.stream().map(File::getAbsolutePath).collect(Collectors.joining(",")),
                            packageName));
                }
            }
        }

        return packageFiles;
    }

    @Override
    public Path getPath() {
        return path;
    }

    @Override
    public File getFile(String fileName) {
        return new File(this.path.toFile(), fileName);
    }

    /**
     * Gets source code dirs.
     *
     * @return the source code dirs
     */
    public Set<File> getSourceCodeDirs() {
        return srcDirs;
    }

    @Override
    public Set<BpmFile> scanBpmFiles() throws XMLStreamException, IOException {
        final Set<File> bpmFiles = new HashSet<>(1);
        for (File srcDir : getSourceCodeDirs()) {
            if (!srcDir.exists()) {
                continue;
            }
            Collection<File> files = FileUtils.listFiles(srcDir, new String[]{"bpm"}, true);
            if (CollectionUtils.isNotEmpty(files)) {
                bpmFiles.addAll(files);
            }
        }

        if (bpmFiles.isEmpty()) {
            return Collections.emptySet();
        }

        final Set<BpmFile> result = new HashSet<>(0);
        for (File bpmFile : bpmFiles) {
            if (new File(bpmFile.getAbsolutePath().replace(".bpm", ".java")).exists()) {
                result.add(new BpmFile(bpmFile, this));
            }
        }

        return result;
    }

    @Override
    public ClassInfo find(String qualifiedTypeName) {
        SymbolReference<ResolvedReferenceTypeDeclaration> parseResult = combinedTypeSolver.tryToSolveType(qualifiedTypeName);
        if (parseResult.isSolved()) {
            ResolvedReferenceTypeDeclaration type = parseResult.getDeclaration().get();
            return new SourceClassInfo2(type);
        }
        return null;
    }

    /**
     * Gets java parser.
     *
     * @return the java parser
     */
    protected JavaParser getJavaParser() {
        JavaParser result = javaParser;
        if (result == null) {
            synchronized (this) {
                result = javaParser;
                if (result == null) {
                    this.combinedTypeSolver = new CombinedTypeSolver();
                    combinedTypeSolver.add(new ReflectionTypeSolver());

                    getSourceDirs().forEach(dir -> combinedTypeSolver.add(new JavaParserTypeSolver(new File(dir.getPath()))));

                    File libDir = this.get3rdPartyFolder();
                    File[] jarFiles = libDir.listFiles(fn -> !fn.getName().endsWith("-sources.jar") && fn.getName().endsWith(".jar") && fn.isFile());
                    if (Objects.nonNull(jarFiles) && jarFiles.length > 0) {
                        Arrays.asList(jarFiles).forEach(jf -> {
                            try {
                                combinedTypeSolver.add(new JarTypeSolver(jf));
                            } catch (IOException e) {
                                log.error("无法找到jar文件, jar file={}", jf.getPath());
                            }
                        });
                    }
                    File[] sourceJarFiles = libDir.listFiles(fn -> fn.getName().endsWith("-sources.jar") && fn.isFile());
                    if (Objects.nonNull(sourceJarFiles) && sourceJarFiles.length > 0) {
                        Arrays.asList(sourceJarFiles).forEach(jf -> {
                            try {
                                combinedTypeSolver.add(new SourceJarTypeSolver(jf.getAbsolutePath()));
                            } catch (IOException e) {
                                log.error("无法找到source jar文件, source jar file={}", jf.getPath());
                            }
                        });
                    }

                    JavaParser javaParser = new JavaParser();
                    javaParser.getParserConfiguration().setSymbolResolver(new JavaSymbolSolver(combinedTypeSolver));
                    result = javaParser;
                    this.javaParser = javaParser;
                }
            }
        }
        return result;
    }

    @Override
    public CompilationUnit parseJava(File javaFile) throws FileNotFoundException {
        ParseResult<CompilationUnit> parseResult = getJavaParser().parse(javaFile);
        if (parseResult.isSuccessful()) {
            Optional<CompilationUnit> result = parseResult.getResult();
            if (result.isPresent()) {
                return result.get();
            }
        }

        return null;
    }

    private Set<File> getSourceDirs() {
        Set<File> sourceDirs = new HashSet<>();
        Set<File> projectSourceCodeDirs = this.getSourceCodeDirs();
        if (null != projectSourceCodeDirs) {
            sourceDirs.addAll(projectSourceCodeDirs);
        }

        Set<File> additionalSourceCodeDirs = this.getIncludeDirs();
        if (null != additionalSourceCodeDirs) {
            sourceDirs.addAll(additionalSourceCodeDirs);
        }
        return sourceDirs;
    }

    public abstract Path getOutputPath();

    /**
     * Gets 3 rd party folder.
     *
     * @return the 3 rd party folder
     */
    public File get3rdPartyFolder() {
        return path.resolve("build").resolve("dependency").toFile();
    }

}
